﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using IWMAG.Scripting.AST.ObjectModel;
using IWMAG.Scripting.Types;
using IWMAG.Scripting.AST.TerminalExpressions;
using IWMAG.Scripting.Helpers;
using System.Reflection;
using IWMAG.Scripting.Interpreter;
using IWMAG.Scripting.AST.Statements;

namespace IWMAG.Scripting.AST.Operators {
    internal class CallOperator : CodeExpression {
        public CodeExpression Method;
        public List<CodeExpression> Arguments = new List<CodeExpression>();
        protected CodeType Type;
        protected bool IsExtern = false;
        protected MethodInfo Invoke;

        public CallOperator(Compiler.Lexeme l, CodeExpression method) : base(l) {
            Method = method;
        }

        public override void Verify(Helpers.CompileTable sym, List<Compiler.IUserInfo> error) {
            foreach (CodeExpression arg in Arguments) {
                arg.Verify(sym, error);
            }

            if (Method is VariableExpression) {
                try {
                    Method.Verify(sym, error);
                } catch (VerifyException) {
                    if (error[error.Count - 1].ToString().StartsWith("Variable") 
                        || error[error.Count - 1].ToString().StartsWith("Latebound")) {
                        error.RemoveAt(error.Count - 1);
                    }
                }
                VariableExpression expr = (VariableExpression) Method;

                if (!sym.Functions.Contains(expr.Name)) {
                    SendError(error, "Call to an unavailable function " + expr.Name);
                    Type = CodeType.Void;
                    return;
                }

                if (sym.Lookahead.Contains(typeof(LateBindOperator)) && sym.Functions[expr.Name].IsAsync) {
                    SendError(error, "Latebound variable may not call asynchronous functions.");
                }

                if (sym.Lookahead.Contains(typeof(IAsyncResult)) && sym.Functions[expr.Name].IsAsync && !sym.Lookahead.Contains(typeof(BeginStatement))) {
                    sym.Lookahead[typeof(IAsyncResult)]();
                }

                Signature me = sym.Functions[expr.Name];
                IsExtern = me.IsExtern;

                for (int i = 0; i < me.Parameters.Length; i++) {
                    if (i + 1 > Arguments.Count) {
                        if (me.Parameters[i].InitialValue == null) {
                            SendError(error, "Not enough arguments to call of " + me.Name);
                        }
                    } else if (!me.Parameters[i].Type.Equals(Arguments[i].TypeOf())) {
                        SendError(error, "Unable to cast " + Arguments[i].TypeOf().ToString() + " to " + me.Parameters[i].Type.ToString() + " for function call " + expr.Name);
                    }
                }

                Type = sym.Dealias(me.ReturnType);
            } else if (Method is MemberOperator) {
                Type[] paramtype = new Type[Arguments.Count];

                try {
                    Method.Verify(sym, error);
                } catch (VerifyException e) {
                    if (!error.LastOrDefault().ToString().StartsWith("Member")) {
                        throw e;
                    } else {
                        error.RemoveAt(error.Count - 1);
                    }
                }

                for (int i = 0; i < Arguments.Count; i++) {
                    paramtype[i] = Marshaller.Marshal(Arguments[i].TypeOf(), sym);
                }

                MemberOperator expr = Method as MemberOperator;
                ParameterInfo[] param = null;
                try {
                    Invoke = expr.ClrType.GetMethod(expr.MemberName, paramtype);
                    param = Invoke.GetParameters();
                } catch {
                    SendError(true, error, "No method " + expr.MemberName + " exists taking the provided arguments");
                }

                for (int i = 0; i < param.Length; i++) {
                    if (i + 1 > Arguments.Count) {
                        if (param[i].DefaultValue is DBNull) {
                            SendError(error, "Not enough arguments to call of " + Invoke.Name);
                        }
                    } else if (Marshaller.Marshal(param[i].ParameterType, sym).Equals(Arguments[i].TypeOf())) {
                        SendError(error, "Unable to cast " + Arguments[i].TypeOf().ToString() + " to " + Marshaller.Marshal(param[i].ParameterType, sym).ToString() + " for function call");
                    }
                }

                Type = Marshaller.Marshal(Invoke.ReturnType, sym);
            }
        }

        public override IEnumerable<Continuation> ValueOf(Interpreter.ScriptEngine engine, RuntimeTable sym, Interpreter.Reference<Interpreter.RunValue> val) {
            if (Method is VariableExpression) {
                string name = (Method as VariableExpression).Name;
                RunValue[] args = new RunValue[Arguments.Count];

                int i = 0;
                foreach (CodeExpression expr in Arguments) {
                    Reference<RunValue> arg = RunValue.NoValue;
                    foreach (Continuation wait in expr.ValueOf(engine, sym, arg)) yield return wait;
                    args[i++] = arg.ReferencedValue;
                }

                if (!IsExtern) {
                    FunctionDeclaration func = sym.Functions[name];
                    RuntimeTable table = new RuntimeTable(sym.Program);
                    table.Parent = engine.Globals;

                    engine.StackCount++;
                    foreach (Continuation wait in func.ValueOf(engine, table, val, args)) yield return wait;
                    engine.StackCount--;
                } else {
                    object[] clrargs = new object[Arguments.Count];
                    for (i = 0; i < Arguments.Count; i++) {
                        clrargs[i] = args[i].Value;
                    }

                    engine.IsExterned = true;
                    object ret = sym.Externs[name].Invoke(clrargs);
                    engine.IsExterned = false;
                    if (ret is IEnumerable<object>) {
                        val.ReferencedValue.Value = new ArrayRunValue(ret as IEnumerable<object>);
                    } else {
                        val.ReferencedValue.Value = ret;
                    }
                }
            } else {
                object[] args = new object[Arguments.Count];

                int i = 0;
                foreach (CodeExpression expr in Arguments) {
                    Reference<RunValue> arg = RunValue.NoValue;
                    foreach (Continuation wait in expr.ValueOf(engine, sym, arg)) yield return wait;
                    args[i++] = arg.ReferencedValue.Value;
                }

                Reference<RunValue> obj = RunValue.NoValue;
                foreach (Continuation wait in Method.ValueOf(engine, sym, obj)) yield return wait;

                engine.StackCount++;
                engine.IsExterned = true;
                object ret = Invoke.Invoke(obj.ReferencedValue.Value, args);
                engine.IsExterned = false;

                if (ret is IEnumerable<object>) {
                    val.ReferencedValue.Value = new ArrayRunValue(ret as IEnumerable<object>);
                } else {
                    val.ReferencedValue.Value = ret;
                }
                engine.StackCount--;
            }

            yield break;
        }

        public override CodeType TypeOf() {
            return Type;
        }
    }
}
